<html>
<head>
<div style="height:0">

<div id="cubic1">
{{3.13,2.74}, {1.08,4.62}, {3.71,0.94}, {2.01,3.81}} 
{{6.71,3.14}, {7.99,2.75}, {8.27,1.96}, {6.35,3.57}} 
{{9.45,10.67}, {10.05,5.78}, {13.95,7.46}, {14.72,5.29}} 
{{3.34,8.98}, {1.95,10.27}, {3.76,7.65}, {4.96,10.64}} 
</div>

</div>

<script type="text/javascript">

var testDivs = [
    cubic1,
];

var scale, columns, rows, xStart, yStart;

var ticks = 10;
var at_x = 13 + 0.5;
var at_y = 23 + 0.5;
var decimal_places = 3;
var tests = [];
var testTitles = [];
var testIndex = 0;
var ctx;
var minScale = 1;
var subscale = 1;
var curveT = -1;
var xmin, xmax, ymin, ymax;

var mouseX, mouseY;
var mouseDown = false;

var draw_deriviatives = false;
var draw_endpoints = true;
var draw_hodo = false;
var draw_hodo2 = false;
var draw_hodo_origin = true;
var draw_midpoint = false;
var draw_tangents = true;
var draw_sequence = true;

function parse(test, title) {
    var curveStrs = test.split("{{");
    if (curveStrs.length == 1)
        curveStrs = test.split("=(");
    var pattern = /[a-z$=]?-?\d+\.*\d*e?-?\d*/g;
    var curves = [];
    for (var c in curveStrs) {
        var curveStr = curveStrs[c];
        var points = curveStr.match(pattern);
        var pts = [];
        for (var wd in points) {
            var num = parseFloat(points[wd]);
            if (isNaN(num)) continue;
            pts.push(num);
        }
        if (pts.length > 2)
            curves.push(pts);
    }
    if (curves.length >= 1) {
        tests.push(curves);
        testTitles.push(title);
    }
}

function init(test) {
    var canvas = document.getElementById('canvas');
    if (!canvas.getContext) return;
    canvas.width = window.innerWidth - 20;
    canvas.height = window.innerHeight - 20;
    ctx = canvas.getContext('2d');
    xmin = Infinity;
    xmax = -Infinity;
    ymin = Infinity;
    ymax = -Infinity;
    for (var curves in test) {
        var curve = test[curves];
        var last = curve.length;
        for (var idx = 0; idx < last; idx += 2) {
            xmin = Math.min(xmin, curve[idx]);
            xmax = Math.max(xmax, curve[idx]);
            ymin = Math.min(ymin, curve[idx + 1]);
            ymax = Math.max(ymax, curve[idx + 1]);
        }
    }
    xmin -= 1;
    var testW = xmax - xmin;
    var testH = ymax - ymin;
    subscale = 1;
    while (testW * subscale < 0.1 && testH * subscale < 0.1) {
        subscale *= 10;
    }
    while (testW * subscale > 10 && testH * subscale > 10) {
        subscale /= 10;
    }
    calcFromScale();
}

function hodograph(cubic) {
    var hodo = [];
    hodo[0] = 3 * (cubic[2] - cubic[0]);
    hodo[1] = 3 * (cubic[3] - cubic[1]);
    hodo[2] = 3 * (cubic[4] - cubic[2]);
    hodo[3] = 3 * (cubic[5] - cubic[3]);
    hodo[4] = 3 * (cubic[6] - cubic[4]);
    hodo[5] = 3 * (cubic[7] - cubic[5]);
    return hodo;
}

function hodograph2(cubic) {
    var quad = hodograph(cubic);
    var hodo = [];
    hodo[0] = 2 * (quad[2] - quad[0]);
    hodo[1] = 2 * (quad[3] - quad[1]);
    hodo[2] = 2 * (quad[4] - quad[2]);
    hodo[3] = 2 * (quad[5] - quad[3]);
    return hodo;
}

function quadraticRootsReal(A, B, C, s) {
    if (A == 0) {
        if (B == 0) {
            s[0] = 0;
            return C == 0;
        }
        s[0] = -C / B;
        return 1;
    }
    /* normal form: x^2 + px + q = 0 */
    var p = B / (2 * A);
    var q = C / A;
    var p2 = p * p;
    if (p2 < q) {
        return 0;
    }
    var sqrt_D = 0;
    if (p2 > q) {
        sqrt_D = sqrt(p2 - q);
    }
    s[0] = sqrt_D - p;
    s[1] = -sqrt_D - p;
    return 1 + s[0] != s[1];
}

function add_valid_ts(s, realRoots, t) {
    var foundRoots = 0;
    for (var index = 0; index < realRoots; ++index) {
        var tValue = s[index];
        if (tValue >= 0 && tValue <= 1) {
            for (var idx2 = 0; idx2 < foundRoots; ++idx2) {
                if (t[idx2] != tValue) {
                    t[foundRoots++] = tValue;
                }
            }
        }
    }
    return foundRoots;
}

function quadraticRootsValidT(a, b, c, t) {
    var s = [];
    var realRoots = quadraticRootsReal(A, B, C, s);
    var foundRoots = add_valid_ts(s, realRoots, t);
    return foundRoots != 0;
}

function find_cubic_inflections(cubic, tValues)
{
    var Ax = src[2] - src[0];
    var Ay = src[3] - src[1];
    var Bx = src[4] - 2 * src[2] + src[0];
    var By = src[5] - 2 * src[3] + src[1];
    var Cx = src[6] + 3 * (src[2] - src[4]) - src[0];
    var Cy = src[7] + 3 * (src[3] - src[5]) - src[1];
    return quadraticRootsValidT(Bx * Cy - By * Cx, (Ax * Cy - Ay * Cx),
            Ax * By - Ay * Bx, tValues);
}

function dx_at_t(cubic, t) {
    var one_t = 1 - t;
    var a = cubic[0];
    var b = cubic[2];
    var c = cubic[4];
    var d = cubic[6];
    return 3 * ((b - a) * one_t * one_t + 2 * (c - b) * t * one_t + (d - c) * t * t);
}

function dy_at_t(cubic, t) {
    var one_t = 1 - t;
    var a = cubic[1];
    var b = cubic[3];
    var c = cubic[5];
    var d = cubic[7];
    return 3 * ((b - a) * one_t * one_t + 2 * (c - b) * t * one_t + (d - c) * t * t);
}

function x_at_t(cubic, t) {
    var one_t = 1 - t;
    var one_t2 = one_t * one_t;
    var a = one_t2 * one_t;
    var b = 3 * one_t2 * t;
    var t2 = t * t;
    var c = 3 * one_t * t2;
    var d = t2 * t;
    return a * cubic[0] + b * cubic[2] + c * cubic[4] + d * cubic[6];
}

function y_at_t(cubic, t) {
    var one_t = 1 - t;
    var one_t2 = one_t * one_t;
    var a = one_t2 * one_t;
    var b = 3 * one_t2 * t;
    var t2 = t * t;
    var c = 3 * one_t * t2;
    var d = t2 * t;
    return a * cubic[1] + b * cubic[3] + c * cubic[5] + d * cubic[7];
}

function calcFromScale() {
    xStart = Math.floor(xmin * subscale) / subscale;
    yStart = Math.floor(ymin * subscale) / subscale;
    var xEnd = Math.ceil(xmin * subscale) / subscale;
    var yEnd = Math.ceil(ymin * subscale) / subscale;
    var cCelsW = Math.floor(ctx.canvas.width / 10);
    var cCelsH = Math.floor(ctx.canvas.height / 10);
    var testW = xEnd - xStart;
    var testH = yEnd - yStart; 
    var scaleWH = 1;
    while (cCelsW > testW * scaleWH * 10 && cCelsH > testH * scaleWH * 10) {
        scaleWH *= 10;
    }
    while (cCelsW * 10 < testW * scaleWH && cCelsH * 10 < testH * scaleWH) {
        scaleWH /= 10;
    }
    
    columns = Math.ceil(xmax * subscale) - Math.floor(xmin * subscale) + 1;
    rows = Math.ceil(ymax * subscale) - Math.floor(ymin * subscale) + 1;
    
    var hscale = ctx.canvas.width / columns / ticks;
    var vscale = ctx.canvas.height / rows / ticks;
    minScale = Math.floor(Math.min(hscale, vscale));
    scale = minScale * subscale;
}

function drawLine(x1, y1, x2, y2) {
    var unit = scale * ticks;
    var xoffset = xStart * -unit + at_x;
    var yoffset = yStart * -unit + at_y;
    ctx.beginPath();
    ctx.moveTo(xoffset + x1 * unit, yoffset + y1 * unit);
    ctx.lineTo(xoffset + x2 * unit, yoffset + y2 * unit);
    ctx.stroke();
}

function drawPoint(px, py) {
    var unit = scale * ticks;
    var xoffset = xStart * -unit + at_x;
    var yoffset = yStart * -unit + at_y;
    var _px = px * unit + xoffset;
    var _py = py * unit + yoffset;
    ctx.beginPath();
    ctx.arc(_px, _py, 3, 0, Math.PI*2, true);
    ctx.closePath();
    ctx.stroke();
}

function drawPointSolid(px, py) {
    drawPoint(px, py);
    ctx.fillStyle = "rgba(0,0,0, 0.4)";
    ctx.fill();
}

function drawLabel(num, px, py) {
    ctx.beginPath();
    ctx.arc(px, py, 8, 0, Math.PI*2, true);
    ctx.closePath();
    ctx.strokeStyle = "rgba(0,0,0, 0.4)";
    ctx.lineWidth = num == 0 || num == 3 ? 2 : 1;
    ctx.stroke();
    ctx.fillStyle = "black";
    ctx.font = "normal 10px Arial";
  //  ctx.rotate(0.001);
    ctx.fillText(num, px - 2, py + 3);
  //  ctx.rotate(-0.001);
}

function drawLabelX(ymin, num, loc) {
    var unit = scale * ticks;
    var xoffset = xStart * -unit + at_x;
    var yoffset = yStart * -unit + at_y;
    var px = loc * unit + xoffset;
    var py = ymin * unit + yoffset  - 20;
    drawLabel(num, px, py);
}

function drawLabelY(xmin, num, loc) {
    var unit = scale * ticks;
    var xoffset = xStart * -unit + at_x;
    var yoffset = yStart * -unit + at_y;
    var px = xmin * unit + xoffset - 20;
    var py = loc * unit + yoffset;
    drawLabel(num, px, py);
}

function drawHodoOrigin(hx, hy, hMinX, hMinY, hMaxX, hMaxY) {
    ctx.beginPath();
    ctx.moveTo(hx, hy - 100);
    ctx.lineTo(hx, hy);
    ctx.strokeStyle = hMinY < 0 ? "green" : "blue";
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(hx, hy);
    ctx.lineTo(hx, hy + 100);
    ctx.strokeStyle = hMaxY > 0 ? "green" : "blue";
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(hx - 100, hy);
    ctx.lineTo(hx, hy);
    ctx.strokeStyle = hMinX < 0 ? "green" : "blue";
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(hx, hy);
    ctx.lineTo(hx + 100, hy);
    ctx.strokeStyle = hMaxX > 0 ? "green" : "blue";
    ctx.stroke();
}

function logCurves(test) {
    for (curves in test) {
        var curve = test[curves];
        if (curve.length != 8) {
            continue;
        }
        var str = "{{";
        for (i = 0; i < 8; i += 2) {
            str += curve[i].toFixed(2) + "," + curve[i + 1].toFixed(2);
            if (i < 6) {
                str += "}, {";
            }
        }
        str += "}}";
        console.log(str);
    }
}

function scalexy(x, y, mag) {
    var length = Math.sqrt(x * x + y * y);
    return mag / length;
}

function drawArrow(x, y, dx, dy) {
    var unit = scale * ticks;
    var xoffset = xStart * -unit + at_x;
    var yoffset = yStart * -unit + at_y;
    var dscale = scalexy(dx, dy, 1);
    dx *= dscale;
    dy *= dscale;
    ctx.beginPath();
    ctx.moveTo(xoffset + x * unit, yoffset + y * unit);
    x += dx;
    y += dy;
    ctx.lineTo(xoffset + x * unit, yoffset + y * unit);
    dx /= 10;
    dy /= 10;
    ctx.lineTo(xoffset + (x - dy) * unit, yoffset + (y + dx) * unit);
    ctx.lineTo(xoffset + (x + dx * 2) * unit, yoffset + (y + dy * 2) * unit);
    ctx.lineTo(xoffset + (x + dy) * unit, yoffset + (y - dx) * unit);
    ctx.lineTo(xoffset + x * unit, yoffset + y * unit);
    ctx.strokeStyle = "rgba(0,75,0, 0.4)";
    ctx.stroke();
}

function draw(test, title) {
    ctx.fillStyle = "rgba(0,0,0, 0.1)";
    ctx.font = "normal 50px Arial";
    ctx.fillText(title, 50, 50);
    ctx.font = "normal 10px Arial";
    var unit = scale * ticks;
  //  ctx.lineWidth = "1.001"; "0.999";
    var xoffset = xStart * -unit + at_x;
    var yoffset = yStart * -unit + at_y;

    for (curves in test) {
        var curve = test[curves];
        if (curve.length != 8) {
            continue;
        }
        ctx.lineWidth = 1;
        if (draw_tangents) {
            ctx.strokeStyle = "rgba(0,0,255, 0.3)";
            drawLine(curve[0], curve[1], curve[2], curve[3]);
            drawLine(curve[2], curve[3], curve[4], curve[5]);
            drawLine(curve[4], curve[5], curve[6], curve[7]);
        }
        if (draw_deriviatives) {
            var dx = dx_at_t(curve, 0);
            var dy = dy_at_t(curve, 0);
            drawArrow(curve[0], curve[1], dx, dy);
            dx = dx_at_t(curve, 1);
            dy = dy_at_t(curve, 1);
            drawArrow(curve[6], curve[7], dx, dy);
            if (draw_midpoint) {
                var midX = x_at_t(curve, 0.5);
                var midY = y_at_t(curve, 0.5);
                dx = dx_at_t(curve, 0.5);
                dy = dy_at_t(curve, 0.5);
                drawArrow(midX, midY, dx, dy);
            }
        }
        ctx.beginPath();
        ctx.moveTo(xoffset + curve[0] * unit, yoffset + curve[1] * unit);
        ctx.bezierCurveTo(
            xoffset + curve[2] * unit, yoffset + curve[3] * unit,
            xoffset + curve[4] * unit, yoffset + curve[5] * unit,
            xoffset + curve[6] * unit, yoffset + curve[7] * unit);
        ctx.strokeStyle = "black";
        ctx.stroke();
        if (draw_endpoints) {
            drawPoint(curve[0], curve[1]);
            drawPoint(curve[2], curve[3]);
            drawPoint(curve[4], curve[5]);
            drawPoint(curve[6], curve[7]);
        }
        if (draw_midpoint) {
            var midX = x_at_t(curve, 0.5);
            var midY = y_at_t(curve, 0.5);
            drawPointSolid(midX, midY);
        }
        if (draw_hodo) {
            var hodo = hodograph(curve);
            var hMinX = Math.min(0, hodo[0], hodo[2], hodo[4]);
            var hMinY = Math.min(0, hodo[1], hodo[3], hodo[5]);
            var hMaxX = Math.max(0, hodo[0], hodo[2], hodo[4]);
            var hMaxY = Math.max(0, hodo[1], hodo[3], hodo[5]);
            var hScaleX = hMaxX - hMinX > 0 ? ctx.canvas.width / (hMaxX - hMinX) : 1;
            var hScaleY = hMaxY - hMinY > 0 ? ctx.canvas.height / (hMaxY - hMinY) : 1;
            var hUnit = Math.min(hScaleX, hScaleY);
            hUnit /= 2;
            var hx = xoffset - hMinX * hUnit;
            var hy = yoffset - hMinY * hUnit;
            ctx.moveTo(hx + hodo[0] * hUnit, hy + hodo[1] * hUnit);
            ctx.quadraticCurveTo(
                hx + hodo[2] * hUnit, hy + hodo[3] * hUnit,
                hx + hodo[4] * hUnit, hy + hodo[5] * hUnit);
            ctx.strokeStyle = "red";
            ctx.stroke();
            if (draw_hodo_origin) {
                drawHodoOrigin(hx, hy, hMinX, hMinY, hMaxX, hMaxY);
            }
        }
        if (draw_hodo2) {
            var hodo = hodograph2(curve);
            var hMinX = Math.min(0, hodo[0], hodo[2]);
            var hMinY = Math.min(0, hodo[1], hodo[3]);
            var hMaxX = Math.max(0, hodo[0], hodo[2]);
            var hMaxY = Math.max(0, hodo[1], hodo[3]);
            var hScaleX = hMaxX - hMinX > 0 ? ctx.canvas.width / (hMaxX - hMinX) : 1;
            var hScaleY = hMaxY - hMinY > 0 ? ctx.canvas.height / (hMaxY - hMinY) : 1;
            var hUnit = Math.min(hScaleX, hScaleY);
            hUnit /= 2;
            var hx = xoffset - hMinX * hUnit;
            var hy = yoffset - hMinY * hUnit;
            ctx.moveTo(hx + hodo[0] * hUnit, hy + hodo[1] * hUnit);
            ctx.lineTo(hx + hodo[2] * hUnit, hy + hodo[3] * hUnit);
            ctx.strokeStyle = "red";
            ctx.stroke();
            drawHodoOrigin(hx, hy, hMinX, hMinY, hMaxX, hMaxY);
        }
        if (draw_sequence) {
            var ymin = Math.min(curve[1], curve[3], curve[5], curve[7]);
            for (var i = 0; i < 8; i+= 2) {
                drawLabelX(ymin, i >> 1, curve[i]);
            }
            var xmin = Math.min(curve[0], curve[2], curve[4], curve[6]);
            for (var i = 1; i < 8; i+= 2) {
                drawLabelY(xmin, i >> 1, curve[i]);
            }
        }
    }
}

function drawTop() {
    init(tests[testIndex]);
    redraw();
}

function redraw() {
    ctx.beginPath();
    ctx.rect(0, 0, ctx.canvas.width, ctx.canvas.height);
    ctx.fillStyle="white";
    ctx.fill();
    draw(tests[testIndex], testTitles[testIndex]);
}

function doKeyPress(evt) {
    var char = String.fromCharCode(evt.charCode);
    switch (char) {
    case '2':
        draw_hodo2 ^= true;
        redraw();
        break;
    case 'd':
        draw_deriviatives ^= true;
        redraw();
        break;
    case 'e':
        draw_endpoints ^= true;
        redraw();
        break;
    case 'h':
        draw_hodo ^= true;
        redraw();
        break;
    case 'N':
        testIndex += 9;
    case 'n':
        if (++testIndex >= tests.length)
            testIndex = 0;
        drawTop();
        break;
    case 'l':
        logCurves(tests[testIndex]);
        break;
    case 'm':
        draw_midpoint ^= true;
        redraw();
        break;
    case 'o':
        draw_hodo_origin ^= true;
        redraw();
        break;
    case 'P':
        testIndex -= 9;
    case 'p':
        if (--testIndex < 0)
            testIndex = tests.length - 1;
        drawTop();
        break;
    case 's':
        draw_sequence ^= true;
        redraw();
        break;
    case 't':
        draw_tangents ^= true;
        redraw();
        break;
    }
}

function calcXY() {
    var e = window.event;
	var tgt = e.target || e.srcElement;
    var left = tgt.offsetLeft;
    var top = tgt.offsetTop;
    var unit = scale * ticks;
    mouseX = (e.clientX - left - Math.ceil(at_x) + 1) / unit + xStart;
    mouseY = (e.clientY - top - Math.ceil(at_y)) / unit + yStart;
}

var lastX, lastY;
var activeCurve = [];
var activePt;

function handleMouseClick() {
    calcXY();
}

function initDown() {
    var unit = scale * ticks;
    var xoffset = xStart * -unit + at_x;
    var yoffset = yStart * -unit + at_y;
    var test = tests[testIndex];
    var bestDistance = 1000000;
    activePt = -1;
    for (curves in test) {
        var testCurve = test[curves];
        if (testCurve.length != 8) {
            continue;
        }
        for (var i = 0; i < 8; i += 2) {
            var testX = testCurve[i];
            var testY = testCurve[i + 1];
            var dx = testX - mouseX;
            var dy = testY - mouseY;
            var dist = dx * dx + dy * dy;
            if (dist > bestDistance) {
                continue;
            }
            activeCurve = testCurve;
            activePt = i;
            bestDistance = dist;
        }
    }
    if (activePt >= 0) {
        lastX = mouseX;
        lastY = mouseY;
    }
}

function handleMouseOver() {
    if (!mouseDown) {
        activePt = -1;
        return;
    }
    calcXY();
    if (activePt < 0) {
        initDown();
        return;
    }
    var unit = scale * ticks;
    var deltaX = (mouseX - lastX) /* / unit */;
    var deltaY = (mouseY - lastY) /*/ unit */;
    lastX = mouseX;
    lastY = mouseY;
    activeCurve[activePt] += deltaX;
    activeCurve[activePt + 1] += deltaY;
    redraw();
}

function start() {
    for (i = 0; i < testDivs.length; ++i) {
        var title = testDivs[i].id.toString();
        var str = testDivs[i].firstChild.data;
        parse(str, title);
    }
    drawTop();
    window.addEventListener('keypress', doKeyPress, true);
    window.onresize = function() {
        drawTop();
    }
}

</script>
</head>

<body onLoad="start();">
<canvas id="canvas" width="750" height="500"
    onmousedown="mouseDown = true"
    onmouseup="mouseDown = false"
    onmousemove="handleMouseOver()"
    onclick="handleMouseClick()"
    ></canvas >
</body>
</html>
